import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as url from 'node:url';

const ROOT_DIR = path.dirname(url.fileURLToPath(import.meta.url));
const PRESETS_DIR = path.join(ROOT_DIR, 'preset');

/**
 * module names used internally, usually as element class-name
 * @typedef {{[k: string]: boolean}} Modules
 */

/* used for the locally served .html, that is already merged but not yet inlined */
export const MODULE_DEV = 'dev';

/**
 * declare some modules as optional, only to be included for specific builds
 * @constant
 * @type Modules
 */
export const DEFAULT_MODULES = {
    'api': true,
    'cmd': true,
    'curtain': false,
    'dbg': true,
    'dcz': true,
    'garland': false,
    'ha': true,
    'idb': true,
    'led': true,
    'light': false,
    'lightfox': false,
    'mqtt': true,
    'nofuss': true,
    'ntp': true,
    'ota': true,
    'relay': true,
    'rfb': false,
    'rfm69': false,
    'rpn': true,
    'sch': true,
    'sns': false,
    'telnet': true,
    'thermostat': false,
    'tspk': true,
    [MODULE_DEV]: false,
};

/**
 * special type of build when multiple single-module files are used
 * currently, only possible way to combine both (besides modifying the targets manually)
 * @constant
 * @type Modules
 */
export const MODULES_ALL = Object.fromEntries(
    Object.entries(DEFAULT_MODULES).map(
        ([key, _]) => {
            if (MODULE_DEV === key) {
                return [key, false];
            }

            return [key, true];
        }));
/**
 * always assume dev build is all plus dev
 * @constant
 * @type Modules
 */
export const MODULES_DEV =
    Object.assign({}, MODULES_ALL, {[MODULE_DEV]: true});

/**
 * name mapping for output filename, when specific module is used
 * @typedef {{[k: string]: string}} NamedBuild
 */

/**
 * generic output, usually this includes a single module
 * @constant
 * @type {NamedBuild}
 */
export const NAMED_BUILD = {
    'curtain': 'curtain',
    'garland': 'garland',
    'light': 'light',
    'lightfox': 'lightfox',
    'rfbridge': 'rfb',
    'rfm69': 'rfm69',
    'sensor': 'sns',
    'thermostat': 'thermostat',
};

/**
 * @param {string} name
 * @returns {Modules}
 */
export function makeModules(name) {
    switch (name) {
    case 'all':
        return MODULES_ALL;

    case MODULE_DEV:
        return MODULES_DEV;

    case 'small':
        return DEFAULT_MODULES;
    }

    if (NAMED_BUILD[name] === undefined) {
        throw new Error(`NAMED_BUILD['${name}'] is missing`);
    }

    const out = Object.assign({}, DEFAULT_MODULES);
    out[NAMED_BUILD[name]] = true;

    return out;
}

/**
 * @param {string} name
 * @returns {string}
 */
function nameToConstant(name) {
    return `MODULE_${name.toUpperCase()}`;
}

/**
 * ref. html/src/index.mjs
 * @param {Modules} modules
 */
function asConstants(modules) {
    const entries = Object.entries(modules);
    const exports =
        entries.map(([key, value]) => {
            return [nameToConstant(key), value.toString()];
        })
        .map(([name, value]) => {
            return `export const ${name} = ${value};`;
        });

    return exports.join('\n');
}

/**
 * ref. vite.config.mjs
 * @param {Modules} modules
 */
function asDictionary(modules) {
    const entries = Object.keys(modules)
        .map((name) => [name, nameToConstant(name)])
        .map(([name, value]) => `    '${name}': ${value},`);

    return `export const MODULES = {\n${entries.join('\n')}\n};`;
}

export const MODULE_PRESETS = new Set([
    'all',
    'small',
    'curtain',
    'garland',
    'light',
    'lightfox',
    'rfbridge',
    'rfm69',
    'sensor',
    'thermostat',
    MODULE_DEV,
]);

/** @param {any} e */
function maybeRethrow(e) {
    /** @type {Error?} */
    let rethrow = null;

    if (e instanceof Error) {
        rethrow = e;
    } else {
        rethrow = new Error(e?.toString() ?? 'unknown error');
    }

    return rethrow;
}

/** 
 * @param {string} target
 * @param {string} data
 */
async function maybe_write(target, data) {
    let existing = '';

    /** @type {fs.FileHandle?} */
    let readable = null;

    try {
        readable = await fs.open(target, 'r');
        existing = (await readable.read()).toString();
    } catch (_) {
    } finally {
        if (readable) {
            await readable.close();
        }
    }

    if (existing === data) {
        return;
    }

    try {
        await fs.mkdir(path.dirname(target));
    } catch (_) {
    }

    /** @type {Error?} */
    let rethrow = null;

    /** @type {fs.FileHandle?} */
    let writable = null;

    try {
        writable = await fs.open(target, 'w');
        await writable.write(data);
    } catch (e) {
        rethrow = maybeRethrow(e);
    } finally {
        if (writable) {
            await writable.close();
        }
    }

    if (rethrow) {
        throw rethrow;
    }
}

/**
 * @param {string} name
 * @param {string} [outdir]
 */
async function builder(name, outdir = PRESETS_DIR) {
    if (!outdir) {
        outdir = PRESETS_DIR;
    }

    const target_dir = path.join(outdir, name);

    const modules = makeModules(name);
    const targets = [
        [
            path.join(target_dir, 'constants.mjs'),
            [asConstants(modules), asDictionary(modules)].join('\n'),
        ],
    ];

    let out = `Written: ${target_dir}`;

    try {
        for (const [target, data] of targets) {
            await maybe_write(target, data);
        }
    } catch (e) {
        out = `Error ${target_dir}: ${(e ?? 'unknown error').toString()}`;
        for (const [target] of targets) {
            await fs.rm(target, {force: true});
        }
    }

    return out;
}

export async function build() {
    return Promise.all(Array.from(MODULE_PRESETS).map((x) => builder(x)));
}

//if (process.argv[1] === import.meta.filename) {
//    await build();
//}
